﻿using System;
using System.Threading;
using System.Collections.Generic;

namespace SBPweb.PROvibe.Server.Framework.Utilities.Patterns
{

    /// <summary>
    /// Provides services of Monitor static class, but while allowing better debugging of deadlocks.
    /// </summary>
    public class LockHelper
    {

        #region Constructor logic

        public LockHelper()
            : this(Guid.NewGuid().ToString(), new object())
        {
        }

        public LockHelper(string name)
            : this(name, new object())
        {
        }

        public LockHelper(object lockObject)
            : this(Guid.NewGuid().ToString(), lockObject)
        {
        }

        /// <summary>
        /// Creates a new instance of LockHelper.
        /// </summary>
        /// <param name="name">The name to give the helper. Cannot be null or empty.</param>
        /// <exception cref="ArgumentOutOfRangeException">If name is null or empty.</exception>
        public LockHelper(string name, object lockObject)
        {
            if (string.IsNullOrEmpty(name))
            {
                throw new ArgumentOutOfRangeException("name");
            }

            if (lockObject == null)
            {
                throw new ArgumentNullException("lockObject");
            }

            m_name = name;
            m_lockObject = lockObject;
        }

        public static LockHelper CreateInstance()
        {
            return CreateInstance(Guid.NewGuid().ToString());
        }

        public static LockHelper CreateInstance(string name)
        {
            return new LockHelper(name);
        }

        #endregion

        /// <summary>
        /// Aquires the lock.
        /// </summary>
        /// <returns>An IDisposable that can be used in a C# 'lock' block.</returns>
        /// <example>using(lockHelper.GetLock(){ //do work here }</example>
        public IDisposable GetLock()
        {
            try
            {
                Monitor.Enter(m_lockObject);

                Thread currentThread = Thread.CurrentThread;

                m_threadStack.Push(currentThread);
                if (string.IsNullOrEmpty(currentThread.Name))
                {
                    m_owningThreadName = string.Format("Unnamed - ManagedThreadId:{0}", currentThread.ManagedThreadId);
                }
                else
                {
                    m_owningThreadName = currentThread.Name;
                }

                return new Unlocker(this);
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        /// <summary>
        /// Proxy for the Monitor.Pulse method on the current instance.
        /// </summary>
        public void Pulse()
        {
            Monitor.Pulse(m_lockObject);
        }

        /// <summary>
        /// Proxy for the Monitor.Wait method on the current instance.
        /// </summary>
        public void Wait()
        {
            Monitor.Wait(m_lockObject);
        }

        /// <summary>
        /// Check to see if the calling thread hold this lock on this LockHelper.
        /// </summary>
        /// <returns>True if it holds the lock. False otherwise.</returns>
        public bool CheckAccess()
        {
            return (m_threadStack.Count > 0) &&
                (Thread.CurrentThread == m_threadStack.Peek());
        }

        /// <summary>
        /// Throws an exception if CheckAccess() returns false.
        /// </summary>
        /// <exception cref="CommonException">If CheckAccess would throw an exception.</exception>
        public void VerifyAccess()
        {
            if (!CheckAccess())
            {
                throw new Exception("Code was run that does not have the nessesary lock.");
            }
        }

        private void unlock()
        {
            VerifyAccess();

            //ideally, this work would be done 'after' Exit, to make sure 'Exit' succeeds
            //BUT, it must be done while the object is locked, or we'll have conflicts
            //with the code that is run after Monitor.Enter

            m_threadStack.Pop();
            if (m_threadStack.Count > 0)
            {
                Thread currentThread = m_threadStack.Peek();
                if (string.IsNullOrEmpty(currentThread.Name))
                {
                    m_owningThreadName = string.Format("Unnamed - ManagedThreadId:{0}", currentThread.ManagedThreadId);
                }
                else
                {
                    m_owningThreadName = currentThread.Name;
                }
            }
            else
            {
                m_owningThreadName = null;
            }

            Monitor.Exit(m_lockObject);
        }

        private string m_owningThreadName;

        private readonly Stack<Thread> m_threadStack = new Stack<Thread>();
        private readonly object m_lockObject;
        private readonly string m_name;

        private class Unlocker : IDisposable
        {
            public Unlocker(LockHelper owner)
            {
                if (owner == null)
                {
                    throw new ArgumentNullException("owner");
                }

                m_owner = owner;
            }

            public void Dispose()
            {
                lock (m_lockObject)
                {
                    if (m_owner != null)
                    {
                        m_owner.unlock();

                        //do this after the unlock to ensure that it succeeds
                        m_owner = null;
                    }
                    else
                    {
                        throw new Exception("Cannot Dispose the object returned from GetLock more than once.");
                    }
                }
            }

            private readonly object m_lockObject = new object();
            private LockHelper m_owner;
        }
    }
    
}